'''
XML Config Parser. An XML parser for config files.
Copyright (C) 2006  Jonathan Claggett

This library is free software; you can redistribute it and/or modify it under
the terms of the GNU Lesser General Public License as published by the Free
Software Foundation; either version 2.1 of the License, or (at your option) any
later version.

This library is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
details.

You should have received a copy of the GNU Lesser General Public License along
with this library; if not, write to the Free Software Foundation, Inc., 51
Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

You may contact the author at <jonathan@claggett.org>
'''
if __debug__: import pdb

import cast
from search_list import SearchList, All, Any, One, Seq

class Stack(list):
    'Minor syntactic sugar around a list'
    def get_top(self): return self[-1]
    def set_top(self, value): self[-1] = value
    top = property(get_top, set_top, None, 'The top item on the stack')

    def get_bottom(self): return self[0]
    bottom = property(get_bottom, None, None, 'The bottom item on the stack')

    push = list.append # push() is an alias of append().

class Parser(object):
    '''
    XML Config Parser. An XML parser with the goal of being easy to use when
    parsing relatively simple XML configuration files. The basic idea is that a
    hierarchy of element classes are defined each with their own start, char and
    end handlers for element specific actions.
    '''

    def __init__(self, element, model = None):

        # Define a trivial root element which contains the passed in element
        # as its only child.
        class Root(Element):
            elements = Seq(element)
        self.stack = Stack()
        self.stack.push(Root(model))

        # Create a casting object
        self.cast = cast.Cast()

        # Create the expat parser and hook up the needed callbacks.
        from xml.parsers.expat import ParserCreate
        self.parser = ParserCreate()
        self.parser.StartElementHandler = self.start_element_handler
        self.parser.CharacterDataHandler = self.character_data_handler
        self.parser.EndElementHandler = self.end_element_handler

    def parse_text(self, text, isfinal = True):
        self.parser.Parse(text, isfinal)
        return self.stack.bottom.model

    def parse_file(self, file):
        self.parser.ParseFile(file)
        return self.stack.bottom.model

    def start_element_handler(self, name, attrs):
        self.stack.push( self.stack.top.get_element(name) )

        self.stack.top.start( **dict(
                [ ( str(k), self.cast(v) ) for k,v in attrs.items() ] ) )

    def character_data_handler(self, text):
        self.stack.top.char(text)

    def end_element_handler(self, name):
        self.stack.pop().end()

# Base element object 
class Element(object):
    '''
    Element is the parent class of Element classes used to handle specific XML
    elements.
    '''

    elements = Any()

    def __init__(self, model=None):
        if model is None:
            self.model = self.new_model()
        else:
            self.model = model

        self.submodel = self.model

    def get_element(self, name):
        element = self.elements.find(name)(self.submodel)

        # If a model hasn't been given to us, use the subelement's
        # model as our own.
        if self.model is None:
            self.model = self.submodel = element.model

        return element

    @classmethod
    def get_name(cls):
        if hasattr(cls, 'name'):
            return cls.name
        else:
            return cls.__name__.lower()

    @classmethod
    def parse_text(cls, text, model=None):
        '''
        Parse a text string using the given element class and optional model.
        Returns the populated model based on the contents of the text string.
        '''
        return Parser(cls, model).parse_text(text)

    @classmethod
    def parse_file(cls, file, model=None):
        '''
        Parse a file using the given element class and optional model.
        Returns the populated model based on the contents of the file.
        '''
        return Parser(cls, model).parse_file(file)

    @staticmethod
    def new_model(): return None

    @staticmethod
    def start(**attrs): pass

    @staticmethod
    def char(text): pass

    @staticmethod
    def end(): pass

# Unit Testing
if __debug__:
    import unittest

    class TextModel(object):
        def __init__(self): self.text = ''

    class TestParser(unittest.TestCase):

        def test_parsing_demo_1(self):

            class Print(Element):
                def new_model(self):
                    return TextModel()

                def start(self, newline=False):
                    if newline: self.newline = '\n'
                    else: self.newline = ''

                def char(self, text):
                    self.model.text = text + self.newline

            model = Print.parse_text( text = u'''
                <print newline="true">Hello World!</print>
                ''' )
            self.assertEqual(model.text, u'''Hello World!\n''')

            model = Print.parse_text( u'<print>Hello World!</print>' )
            self.assertEqual(model.text, u'''Hello World!''')

        def test_parsing_demo_2(self):
            class Book(Element):
                def start(self, title = 'Unknown Book', auto=False):
                    if auto: self.model.text += '(Autobiography) '
                    self.model.text += title + '\n'
                def char(self, text):
                    self.model.text += '\t' + text + '\n'

            class Catalog(Element):
                name = 'catalog'
                elements = Any(Book)
                def new_model(self):
                    return TextModel()
                def start(self, title = 'Unknown Catalog'):
                    self.title = title
                    self.model.text += '\nBegin Catalog: ' + self.title + '\n\n'
                def end(self):
                    self.model.text += '\nEnd Catalog: ' + self.title

            model = Catalog.parse_text( text = u'''
                <catalog title="An Incomplete Catalog of Penguin Books">
                <book title="Illiad, The" auto="TruE">ipsum lorem</book>
                <book title="Swallows and Amazons">ipsum lorem</book>
                </catalog>
                ''')
            self.assertEqual( model.text, u'''
Begin Catalog: An Incomplete Catalog of Penguin Books

(Autobiography) Illiad, The
	ipsum lorem
Swallows and Amazons
	ipsum lorem

End Catalog: An Incomplete Catalog of Penguin Books''')

    if __name__ == '__main__':
        unittest.main()
        #test_support.run_unittest( cast.TestCast, TestParser )

# vim: ai et sw=4 ts=4
